TimerLibGroup = {};

local GetTime = GetTime;
local max = math.max;
local huge = math.huge;
local fmod = math.fmod;
local gsub = string.gsub;
local match = string.match;
local format = string.format;

local acquireGroupFrame, cachedTables;

local timerSortFunc = function(a, b)
    if a.priority ~= b.priority then
        return (a.priority or huge) < (b.priority or huge);
    end
    if a.hidden ~= b.hidden then
        return b.hidden;
    end
    local method, direction = a.group.timerSortMethod, a.group.timerSortDirection;
    local time = GetTime();
    local aComp, bComp, flag;
    if method == "Time Remaining" then
        if  (a.duration or b.duration) then
            aComp = a.duration and (a.duration - time + a.time) or huge;
            bComp = b.duration and (b.duration - time + b.time) or huge;
        else
            flag = true;
            aComp, bComp = a.name, b.name;
        end
    elseif method == "Percent Remaining" then
        if  (a.duration or b.duration) then
            aComp = a.duration and ((a.duration - time + a.time) / a.duration) or huge;
            bComp = b.duration and ((b.duration - time + b.time) / b.duration) or huge;
        else
            flag = true;
            aComp, bComp = a.name, b.name;
        end
    elseif method == "Time Added" then
        if  (a.duration or b.duration) then
            aComp = a.duration and (a.time) or huge;
            bComp = b.duration and (b.time) or huge;
        else
            flag = true;
            aComp, bComp = a.name, b.name;
        end
    elseif method == "Alphabetical" then
        aComp, bComp = a.name, b.name;
    end
    
    if flag then
        return aComp < bComp;
    end
    if direction == "D" then
        if aComp ~= bComp then
            return (bComp < aComp);
        else
            return (b.name < a.name);
        end
    else 
        if aComp ~= bComp then
            return (aComp < bComp);
        else
            return (a.name < b.name);
        end
    end
end

local groupFuncs = {
    AddTimer = function(self, timer)
        table.insert(self.timers, timer);
        timer.group = self;
        timer.frame:SetParent(self.frame);
        timer:DrawFrame();
        for t in self:GetTimers() do
            if t.name == timer.name and t ~= timer and t.isGhost then
                self:DestroyTimer(t);
            end
        end
        self:Resize(self:SortTimers());
        self.anchor:ReanchorGroups();
    end,
    
    DestroyTimer = function(self, timer)
        for i, t in ipairs(self.timers) do
            if (t == timer) then 
                table.remove(self.timers, i);
                self:Resize();
                self.anchor:ReanchorGroups();
                if (#self.timers == 0) then
                    self:Destruct();
                end
                t.frame:SetParent(nil);
                t:Destruct();
                break;
            end
        end
    end,
    
    RemoveTimer = function(self, timer)
        for i, t in ipairs(self.timers) do
            if (t == timer) then 
                if not t.isGhost then t:Remove(); end
                break;
            end
        end
        
        local hasData = false;
        for i, t in ipairs(self.timers) do
            if t.data then
                hasData = true;
                break;
            end
        end
        if hasData then
            self.allGhost = false;
        else
            self.allGhost = self.allGhost or GetTime();
        end
        hasData = hasData or type(self.data) == "string";
        if not hasData then
            self.data = nil;
        end
    end,
    
    iteratorState = 0,
    iteratorFunc = function(self, timer)
        self.iteratorState = self.iteratorState - 1;
        if self.iteratorState > 0 then
            return self.timers[self.iteratorState];
        end
    end,
    
    GetTimers = function(self)
        self.iteratorState = #self.timers + 1;
        return self.iteratorFunc, self;
    end,
    
    SortTimers = function(self)
        table.sort(self.timers, timerSortFunc);
        local correctlySorted = true;
        for i, t in ipairs(self.timers) do
            if t.index ~= i then
                correctlySorted = false;
            end
            t.index = i;
        end
        --reanchor
        if not correctlySorted then
            return self:ReanchorTimers();
        end
    end,
    
    MoveTimer = function(self, timer, group, groupdata, anchorName, groupPriority, groupHidden)
        local anchor = TimerLib.anchors[anchorName] or TimerLibAnchor:NewAnchor(anchorName);
        TimerLib.anchors[anchorName] = anchor;
        if anchor:FindGroup(groupdata) == self then return self; end
        
        for i, t in ipairs(self.timers) do
            if t == timer then
                table.remove(self.timers, i);
                self:Resize();
                if (#self.timers == 0) then
                    self:Destruct();
                end
                
                local hasData = false;
                for i, t in ipairs(self.timers) do
                    if t.data then
                        hasData = true;
                        break;
                    end
                end
                if hasData then
                    self.allGhost = false;
                else
                    self.allGhost = self.allGhost or GetTime();
                end
                hasData = hasData or type(self.data) == "string";
                if not hasData then
                    self.data = nil;
                end
                
                if group and group.data == groupdata then
                    group:AddTimer(timer);
                else
                    group = anchor:AddTimer(timer, groupdata, groupPriority, groupHidden);
                end
                return group;
            end
        end
    end,
    
    Resize = function(self, timersWidth, timersHeight)
        local num = #self.timers;
        if (num > 0) then
            local anchor = self.frame.TimerAnchor;
            local name = self.frame.Name;
            name:ClearAllPoints();
            anchor:ClearAllPoints();
            local width, height;
            
            local timerDirection = self:Get("timerDirection");
            local groupDirection = self:Get("groupDirection");
            local justify = self:Get("timerJustification");
            
            local horizGroups = (groupDirection == "left" or groupDirection == "right");
            local moveName = self:Get("moveName") or horizGroups;
            local showingName = self:Get("displayNames") or (self:Get("displayIcons") and self.icon);
            local ref1, ref2, obj, ref3, ref4, x1, y1, x2, y2;
            local nameX, nameY = 0, 0;
            if timerDirection == "up" then
                if showingName then
                    if moveName then
                        if groupDirection == "left" or groupDirection == "right" then
                            ref1, obj, ref2, x1, y1 = "BOTTOM", self.frame, "BOTTOM", 0, 0;
                            ref3, ref4, x2, y2 = "TOP", "BOTTOM", 0, -5;
                        else
                            ref1, obj, ref2, x1, y1 = "BOTTOM", name, "TOP", 0, 5;
                            ref3, ref4, x2, y2 = "BOTTOM", "BOTTOM", 0, 0;
                            nameY = name:GetHeight() + 5;
                        end
                    else
                        ref1, obj, ref2, x1, y1 = "BOTTOM", self.frame, "BOTTOM", 0, 0;
                        if justify == "right" then
                            ref3, ref4, x2, y2 = "BOTTOMLEFT", "BOTTOMRIGHT", 5, 0;
                        else
                            ref3, ref4, x2, y2 = "BOTTOMRIGHT", "BOTTOMLEFT", -5, 0;
                        end
                    end
                else
                    ref1, obj, ref2, x1, y1 = "BOTTOMLEFT", self.frame, "BOTTOMLEFT", 0, 0;
                end
            elseif timerDirection == "down" then
                if showingName then
                    if moveName then
                        if groupDirection == "left" or groupDirection == "right" then
                            ref1, obj, ref2, x1, y1 = "TOP", self.frame, "TOP", 0, 0;
                            ref3, ref4, x2, y2 = "BOTTOM", "TOP", 0, 5;
                        else
                            ref1, obj, ref2, x1, y1 = "TOP", name, "BOTTOM", 0, -5;
                            ref3, ref4, x2, y2 = "TOP", "TOP", 0, 0;
                            nameY = name:GetHeight() + 5;
                        end
                    else
                        ref1, obj, ref2, x1, y1 = "BOTTOM", self.frame, "BOTTOM", 0, 0;
                        if justify == "right" then
                            ref3, ref4, x2, y2 = "TOPLEFT", "TOPRIGHT", 5, 0;
                        else
                            ref3, ref4, x2, y2 = "TOPRIGHT", "TOPLEFT", -5, 0;
                        end
                    end
                else
                    ref1, obj, ref2, x1, y1 = "TOPLEFT", self.frame, "TOPLEFT", 0, 0;
                end
            elseif timerDirection == "left" then
                if showingName then
                    if moveName then
                        if groupDirection == "up" then
                            ref1, obj, ref2, x1, y1 = "BOTTOMRIGHT", name, "TOPRIGHT", 0, 5;
                            ref3, ref4, x2, y2 = "BOTTOMRIGHT", "BOTTOMRIGHT", 0, 0;
                        else
                            ref1, obj, ref2, x1, y1 = "TOPRIGHT", name, "BOTTOMRIGHT", 0, -5;
                            ref3, ref4, x2, y2 = "TOPRIGHT", "TOPRIGHT", 0, 0;
                        end
                        nameY = name:GetHeight() + 5;
                    else
                        if groupDirection == "up" then
                            ref1, obj, ref2, x1, y1 = "BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", 0, 0;
                            ref3, ref4, x2, y2 = "BOTTOMLEFT", "BOTTOMRIGHT", 5, 0;
                        else
                            ref1, obj, ref2, x1, y1 = "TOPRIGHT", self.frame, "TOPRIGHT", 0, 0;
                            ref3, ref4, x2, y2 = "TOPLEFT", "TOPRIGHT", 5, 0;
                        end
                    end
                else
                    ref1, obj, ref2, x1, y1 = "BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", 0, 0;
                end
            elseif timerDirection == "right" then
                if showingName then
                    if moveName then
                        if groupDirection == "up" then
                            ref1, obj, ref2, x1, y1 = "BOTTOMLEFT", name, "TOPLEFT", 0, 5;
                            ref3, ref4, x2, y2 = "BOTTOMLEFT", "BOTTOMLEFT", 0, 0;
                        else
                            ref1, obj, ref2, x1, y1 = "TOPLEFT", name, "BOTTOMLEFT", 0, -5;
                            ref3, ref4, x2, y2 = "TOPLEFT", "TOPLEFT", 0, 0;
                        end
                        nameY = name:GetHeight() + 5;
                    else
                        if groupDirection == "up" then
                            ref1, obj, ref2, x1, y1 = "BOTTOMLEFT", self.frame, "BOTTOMLEFT", 0, 0;
                            ref3, ref4, x2, y2 = "BOTTOMRIGHT", "BOTTOMLEFT", -5, 0;
                        else
                            ref1, obj, ref2, x1, y1 = "TOPLEFT", self.frame, "TOPLEFT", 0, 0;
                            ref3, ref4, x2, y2 = "TOPRIGHT", "TOPLEFT", -5, 0;
                        end
                    end
                else
                    ref1, obj, ref2, x1, y1 = "BOTTOMLEFT", self.frame, "BOTTOMLEFT", 0, 0;
                end
            end
            
            anchor:SetPoint(ref1, obj, ref2, x1, y1);
            if showingName then
                name:Show();
                name:SetPoint(ref3, self.frame, ref4, x2, y2);
            else
                name:Hide();
            end
            
            if timersWidth then
                width, height = timersWidth, timersHeight;
            else
                width, height = self:ReanchorTimers();
            end
            anchor:SetWidth(width);
            anchor:SetHeight(height);
            self.frame:SetWidth(math.max(width + nameX, showingName and horizGroups and name:GetWidth() or 0));
            self.frame:SetHeight(math.max(height + nameY, showingName and (not moveName) and name:GetHeight() or 0));
            if self:Get("centering") ~= "none" then
                self.anchor:Center();
            end
        end
    end,
    
    ReanchorTimers = function(self)
        local num = #self.timers;
        if num > 0 then
            local dir = self:Get("timerDirection");
            local overflowDir = self:Get("overflowDirection");
            local spacing = self:Get("timerSpacing");
            local overflowPoint = self:Get("overflowPoint");
            local justify = self:Get("timerJustification");
            local totalWidth, totalHeight, width, height = 0, 0, 0, 0;
            local numHidden = 0;
            for i = num, 1, -1 do
                if self.timers[i].hidden then
                    numHidden = numHidden + 1;
                else
                    break;
                end
            end
            local lastFrame;
            self.allHidden = (num == numHidden);
            
            for i = num - numHidden + 1, num do
                self.timers[i].frame:Hide();
            end
            num = num - numHidden;
            
            for i = 1, num do
                local frame = self.timers[i].frame;
                frame:Show();
                frame:ClearAllPoints();
                local mod = fmod(i, overflowPoint);
                local ref1, ref2, offset;
                if dir == "up" then
                    ref1, ref2 = "BOTTOMLEFT", "TOPLEFT";
                    if overflowDir == "left" or justify == "right" then
                        ref1, ref2 = "BOTTOMRIGHT", "TOPRIGHT";
                    end
                    offset = spacing;
                elseif dir == "down" then
                    ref1, ref2 = "TOPLEFT", "BOTTOMLEFT";
                    if overflowDir == "left" or justify == "right" then
                        ref1, ref2 = "TOPRIGHT", "BOTTOMRIGHT";
                    end
                    offset = -spacing;
                elseif dir == "left" then
                    ref1, ref2 = "TOPRIGHT", "TOPLEFT";
                    if overflowDir == "up" or justify == "bottom" then
                        ref1, ref2 = "BOTTOMRIGHT", "BOTTOMLEFT";
                    end
                    offset = -spacing;
                elseif dir == "right" then
                    ref1, ref2 = "TOPLEFT", "TOPRIGHT";
                    if overflowDir == "up" or justify == "bottom" then
                        ref1, ref2 = "BOTTOMLEFT", "BOTTOMRIGHT";
                    end
                    offset = spacing;
                end
                
                if (overflowPoint > 0 and mod == 1) or (overflowPoint == 0 and i == 1) or (overflowPoint == 1) then
                    local x, y = 0, 0;
                    x = overflowDir == "left" and -totalWidth or overflowDir == "right" and totalWidth or x;
                    y = overflowDir == "up" and totalHeight or overflowDir == "down" and -totalHeight or y;
                    frame:SetPoint(ref1, self.frame.TimerAnchor, ref1, x, y);
                    width = frame:GetWidth();
                    height = frame:GetHeight();
                elseif dir == "up" or dir == "down" then
                    frame:SetPoint(ref1, lastFrame, ref2, 0, offset);
                    width = max(width, frame:GetWidth());
                    height = height + frame:GetHeight() + spacing;
                elseif dir == "left" or dir == "right" then
                    frame:SetPoint(ref1, lastFrame, ref2, offset, 0);
                    width = width + frame:GetWidth() + spacing;
                    height = max(height, frame:GetHeight());
                end
                lastFrame = frame;
                
                if (overflowPoint > 0 and mod == 0) or (i == num) then
                    if overflowDir == "left" or overflowDir == "right" then
                        totalWidth = totalWidth + width + (i == num and 0 or spacing);
                        totalHeight = max(totalHeight, height);
                    else
                        totalWidth = max(totalWidth, width);
                        totalHeight = totalHeight + height + (i == num and 0 or spacing);
                    end
                end
            end
            
            return totalWidth, totalHeight;
        end
    end,
    
    UpdateData = function(self)
        if type(self.data) == "table" then
            self.icon = self.data.icon;
            self.uniqueID = self.data.uniqueID;
            if self.icon == 0 then
                self.icon = nil;
            end
            if self.data.level and self.data.level ~= 0 then
                local level = self.data.level == -1 and "??" or tostring(self.data.level);
                local groupDirection = self:Get("groupDirection");
                local moveName = self:Get("moveName") or (groupDirection == "left" or groupDirection == "right");
                self.displayText = format("[%s] %s", level, moveName and self.data.name or gsub(self.data.name or "Group", " ", "\n"));
            else
                self.displayText = self.data.name or "Group";
            end
            self.name = self.data.name or "Group";
        else
            self.displayText = self.data;
            self.name = self.displayText;
        end
        
        self:DrawFrame();
    end,
    
    UpdateSettings = function(self)
        for timer in self:GetTimers() do
            timer:UpdateSettings();
        end
        self:DrawFrame();
        self:SortTimers();
    end,
    
    DrawFrame = function(self)
        local method, direction = match(self:Get("timerSortMethod"), "(.*) %((%a)%)");
        self.timerSortMethod = method;
        self.timerSortDirection = direction;
        method, direction = match(self:Get("groupSortMethod"), "(.*) %((%a)%)");
        self.groupSortMethod = method;
        self.groupSortDirection = direction;
        
        local frame = self.frame;
        local displayNames = self:Get("displayNames");
        local displayIcons = self:Get("displayIcons");
        local font = frame.NameText:GetFont();
        local fontHeight = self:Get("nameFontHeight");
        frame.NameText:SetFont(font, fontHeight);
        frame.NameText:SetText(displayNames and self.displayText or "");
        local color = self:Get("nameTextColor");
        frame.NameText:SetTextColor(color.r, color.g, color.b);
        frame.Name:SetWidth(max(displayNames and frame.NameText:GetStringWidth() or 1, (displayIcons and self.icon) and frame.NameIcon:GetWidth() or 1));
        frame.Name:SetHeight(max(displayNames and frame.NameText:GetStringHeight() or 1, (displayIcons and self.icon) and frame.NameIcon:GetHeight() or 1));
        SetRaidTargetIconTexture(self.frame.NameIcon, displayIcons and self.icon or 0);
        frame.Name:EnableMouse(self.anchor.frame.anchorFrame:IsMovable());
        
        self:Resize();
    end,
    
    Get = function(self, ...)
        return self.anchor:Get(...);
    end,
    
    Destruct = function(self)
        table.insert(cachedTables, self);
        self.frame:Hide();
        self.anchor:RemoveGroup(self);
        local anchor = self.anchor;
        local frame = anchor.frame.anchorFrame;
        if self.isMoving and frame:IsMovable() then
            frame:StopMovingOrSizing();
            local x, y = frame:GetLeft(), frame:GetTop();
            anchor:Set("positionX", x);
            anchor:Set("positionY", y);
            self.isMoving = false;
        end
    end,
}

local counter = 0;
function acquireGroupFrame()
    counter = counter + 1;
    local frame = CreateFrame("Frame", "TimerLibGroup" .. counter, nil, "TimerLibGroupFrameTemplate");
    
    local findFrameElement = TimerLib.findFrameElement;
    findFrameElement(frame, "NameIcon");
    findFrameElement(frame, "NameText");
    findFrameElement(frame, "Name");
    findFrameElement(frame, "TimerAnchor");
    --[[
    local t = frame:CreateTexture();
    t:SetAllPoints(true);
    t:SetTexture(0, 1, 0, .5);
    t = frame.Name:CreateTexture();
    t:SetAllPoints(true);
    t:SetTexture(0, 0, 1, .5);--]]
    
    --[[
    local t = frame.TimerAnchor:CreateTexture();
    t:SetAllPoints(true);
    t:SetTexture(0, 1, 0, .5);--]]
    
    return frame;
end

cachedTables = {};
function TimerLibGroup:NewGroup(groupdata)
    local group = table.remove(cachedTables, 1);
    if group then
        TimerLib:ClearTable(group.state);
        TimerLib:ClearTable(group.timers);
        group.frame:Show();
    else
        group = TimerLib:AcquireTable();
        group.timers = TimerLib:AcquireTable();
        group.state = TimerLib:AcquireTable();
        group.frame = acquireGroupFrame();
        group.frame.group = group;
        group.frame.Name.group = group;
        for i, v in pairs(groupFuncs) do
            group[i] = v;
        end
    end
    
    group.time = GetTime();
    group.data = groupdata;
    setmetatable(group, TimerLib.stateMeta);
    return group;
end
